﻿/**
 *  The CLIK ScrollBar displays and controls the scroll position of another component. It adds interactivity to the ScrollIndicator with a draggable thumb button, as well as optional “up” and “down” arrow buttons, and a clickable track. 
    
    <b>Inspectable Properties</b>
    The inspectable properties of the ScrollBar are similar to ScrollIndicator with one addition:
    <ul>
        <li><i>enabled</i>: Disables the component if set to false.</li>
        <li><i>offsetTop</i>: Thumb offset at the top. A positive value moves the thumb's top-most position higher.</li>
        <li><i>offsetBottom</i>: Thumb offset at the bottom. A positive value moves the thumb's bottom-most position lower.</li>
        <li><i>trackMode</i>: When the user clicks on the track with the cursor, the scrollPage setting will cause the thumb to continuously scroll by a page until the cursor is released. The scrollToCursor setting will cause the thumb to immediately jump to the cursor and will also transition the thumb into a dragging mode until the cursor is released.</li>
        <li><i>scrollTarget</i>: Set a TextArea or normal multiline textField as the scroll target to automatically respond to scroll events. Non-text field types have to manually update the ScrollIndicator properties.</li>
        <li><i>visible</i>: Hides the component if set to false.</li>
    </ul>
    
    <b>States</b>
    The ScrollBar, similar to the ScrollIndicator, does not have explicit states. It uses the states of its child elements, the thumb, up, down and track Button components.
    
    
    <b>Events</b>
    All event callbacks receive a single Event parameter that contains relevant information about the event. The following properties are common to all events. <ul>
    <li><i>type</i>: The event type.</li>
    <li><i>target</i>: The target that generated the event.</li></ul>
        
    The events generated by the ScrollBar component are listed below. The properties listed next to the event are provided in addition to the common properties.
    <ul>
        <li><i>ComponentEvent.SHOW</i>: The visible property has been set to true at runtime.</li>
        <li><i>ComponentEvent.HIDE</i>: The visible property has been set to false at runtime.</li>
        <li><i>Event.SCROLL</i>: The scroll position has changed.</li>
    </ul>
*/

/**************************************************************************

Filename    :   ScrollBar.as

Copyright   :   Copyright 2011 Autodesk, Inc. All Rights reserved.

Use of this software is subject to the terms of the Autodesk license
agreement provided at the time of installation or download, or which
otherwise accompanies this software in either electronic or hard copy form.

**************************************************************************/

package scaleform.clik.controls {
    
    import flash.display.MovieClip;
    import flash.events.Event;
    import flash.events.MouseEvent;
    import flash.geom.Point;
    import flash.text.TextField;
    
    import scaleform.clik.constants.ScrollBarTrackMode
    import scaleform.clik.constants.InvalidationType;
    import scaleform.clik.core.UIComponent;
    import scaleform.clik.core.UIComponent;
    import scaleform.clik.events.ButtonEvent;
    import scaleform.clik.events.InputEvent;
    import scaleform.clik.events.ComponentEvent;
    import scaleform.clik.utils.Constraints;
    import scaleform.clik.constants.ConstrainMode;
    import scaleform.clik.constants.ScrollBarDirection;
    
    public class ScrollBar extends ScrollIndicator {
        
    // Constants:
        
    // Public Properties:
        public var trackScrollPageSize:Number = 1; //LM: Should we use _pageSize instead?
        
    // Protected Properties:
        protected var _dragOffset:Point;
        protected var _trackMode:String = ScrollBarTrackMode.SCROLL_PAGE;
        protected var _trackScrollPosition:Number = -1;
        protected var _trackDragMouseIndex:Number = -1;
        
    // UI Elements:
        /** A reference to the up arrow symbol in the ScrollBar, used to decrement the scroll position. */
        public var upArrow:Button;
        /** A reference to the down arrow symbol in the ScrollBar, used to increment the scroll position. */
        public var downArrow:Button;

    // Initialization:
        public function ScrollBar() {
            super();
        }
        
        override protected function initialize():void {
            super.initialize();
            
            var r:Number = rotation;
            rotation = 0;
            
            // The upArrow doesn't need a constraints, since it sticks to top left.
            if (downArrow) {
                constraints.addElement("downArrow", downArrow, Constraints.BOTTOM);
            }
            constraints.addElement("track", track, Constraints.TOP | Constraints.BOTTOM);
            rotation = r;
        }
        
        override protected function preInitialize():void {
            constraints = new Constraints(this, ConstrainMode.REFLOW);
        }
        
    // Public Getter / Setters:
        /**
         * Enable / disable this component. Focus (along with keyboard events) and mouse events will be suppressed if disabled.
         */
        [Inspectable(defaultValue="true")]
        override public function get enabled():Boolean { return super.enabled; }
        override public function set enabled(value:Boolean):void {
            if (enabled == value) { return; }
            super.enabled = value;
            
            gotoAndPlay(enabled ? "default" : "disabled"); // setState?
            invalidate(InvalidationType.STATE);
        }
        
        /** 
         * Set the scroll position to a number between the minimum and maximum.
         */
        override public function get position():Number { return _position; }
        override public function set position(value:Number):void {
            value = Math.round(value); // The value is rounded so that the scrolling represents the position properly. Particularly for TextFields.
            if (value == position) { return; }
            super.position = value;
            updateScrollTarget();
        }
        
        /** 
         * Set the behavior when clicking on the track. The scrollPage value will move the grip by a page in the direction of the click. The scrollToCursor value will move the grip to the exact position that was clicked and become instantly draggable.
         */
        [Inspectable(type="String", enumeration="scrollPage,scrollToCursor", defaultValue="scrollPage")]
        public function get trackMode():String { return _trackMode; }
        public function set trackMode(value:String):void {
            if (value == _trackMode) { return; }
            _trackMode = value;
            if (initialized) { 
                track.autoRepeat = (trackMode == ScrollBarTrackMode.SCROLL_PAGE);
            }
        }
        
    // Public Methods:
        /** @exclude */
        override public function get availableHeight():Number {
            return track.height - thumb.height + offsetBottom + offsetTop;
        }
        
        /** @exclude */
        override public function toString():String {
            return "[CLIK ScrollBar " + name + "]";
        }
        
    // Protected Methods:
        override protected function configUI():void {
            super.configUI();
            mouseEnabled = mouseChildren = enabled;
            tabEnabled = tabChildren = _focusable;
            
            addEventListener(MouseEvent.MOUSE_WHEEL, handleMouseWheel, false, 0, true);
            addEventListener(InputEvent.INPUT, handleInput, false, 0, true);
            
            if (upArrow) {
                upArrow.addEventListener(ButtonEvent.CLICK, handleUpArrowClick, false, 0, true);
                upArrow.addEventListener(ButtonEvent.PRESS, handleUpArrowPress, false, 0, true);
                upArrow.focusTarget = this;
                upArrow.autoRepeat = true;
            }
            if (downArrow) {
                downArrow.addEventListener(ButtonEvent.CLICK, handleDownArrowClick, false, 0, true);
                downArrow.addEventListener(ButtonEvent.PRESS, handleDownArrowPress, false, 0, true);
                downArrow.focusTarget = this;
                downArrow.autoRepeat = true;
            }
            
            thumb.addEventListener(MouseEvent.MOUSE_DOWN, handleThumbPress, false, 0, true);
            thumb.focusTarget = this;
            thumb.lockDragStateChange = true;
            
            track.addEventListener(MouseEvent.MOUSE_DOWN, handleTrackPress, false, 0, true);
            track.addEventListener(ButtonEvent.CLICK, handleTrackClick, false, 0, true);
            
            if (track is UIComponent) { (track as UIComponent).focusTarget = this; }
            // Uses tabChildren instead of setting tabEnabled on buttons.
            track.autoRepeat = (trackMode == ScrollBarTrackMode.SCROLL_PAGE);
        }
        
        protected function scrollUp():void {
            position -= _pageScrollSize;
        }
        
        protected function scrollDown():void {
            position += _pageScrollSize;
        }
        
        override protected function drawLayout():void {
            // NFM: Reset the y position of the thumb so that the _height is valid when the constraints are updated.
            thumb.y = track.y - offsetTop;
            
            if (isHorizontal) {
                constraints.update(_height, _width);
            } else {
                constraints.update(_width, _height);
            }

            // This is a fix for a bug in Flash Player/Scaleform where setting the width/height of a rotated DisplayObject
            // will cause errors in its width/height. These width/height changes can exponentially? impact one another 
            // depending on the current rotation when and order in which they are applied. 
            // There are actually a few different bugs at play, but the short version for ScrollBar is that the
            // DisplayObject.width will be screwed up when we change the size/scale of the ScrollBar in setActualSize and
            // remain incorrect when constraints.update() is called, regardless of explicit calls to set super.width in UIComponent.
            // Once all of these changes have been made, we can work around the problem by rescaling the incorrect width 
            // back to where it *should* be, regardless of how Flash Player/Scaleform has calculated the value.
            // DisplayObject.scaleX and DisplayObject.scaleY still affect the original axises of the DisplayObject,
            // which is why they work here.
            if (isHorizontal && actualWidth != width) {
                var yScaleFix:Number = width / actualWidth;
                scaleY = yScaleFix;
            }
        }
        
        override protected function updateThumb():void {
            var per:Number = Math.max(1, _maxPosition - _minPosition + _pageSize);
            var trackHeight:Number = track.height + offsetTop + offsetBottom; // Uses the track as the height indicator, since ScrollIndicator can use just gfx as a track.
            thumb.height = Math.max(_minThumbSize, Math.min(trackHeight, _pageSize / per * trackHeight));
            if (thumb is UIComponent) { (thumb as UIComponent).validateNow(); }
            updateThumbPosition();
        }
        
        override protected function updateThumbPosition():void {
            var percent:Number = (_position - _minPosition) / (_maxPosition - _minPosition);
            var top:Number = track.y - offsetTop;
            var yPos:Number = Math.round(percent * availableHeight + top);
            
            thumb.y = Math.max(top, Math.min(track.y + track.height - thumb.height + offsetBottom, yPos));
            thumb.visible = !(isNaN(percent) || isNaN(_pageSize) || _maxPosition <= 0 || _maxPosition == Infinity);
            
            var showThumb:Boolean = thumb.visible && enabled;
            if (upArrow) {
                upArrow.enabled = showThumb && (_position > _minPosition);
                upArrow.validateNow();
            }
            if (downArrow) {
                downArrow.enabled = showThumb && (_position < _maxPosition);
                downArrow.validateNow();
            }
            track.enabled = track.mouseEnabled = showThumb;
        }
        
        protected function handleUpArrowClick(event:ButtonEvent):void {
            if (event.isRepeat) { 
                scrollUp();
            }
        }
        protected function handleUpArrowPress(event:ButtonEvent):void {
            scrollUp();
        }
        
        protected function handleDownArrowClick(event:ButtonEvent):void {
            if (event.isRepeat) { 
                scrollDown();
            }
        }
        protected function handleDownArrowPress(event:ButtonEvent):void {
            scrollDown();
        }
        
        protected function handleThumbPress(event:Event):void {
            if (_isDragging) { return; }
            _isDragging = true;
            stage.addEventListener(MouseEvent.MOUSE_MOVE, doDrag, false, 0, true);
            stage.addEventListener(MouseEvent.MOUSE_UP, endDrag, false, 0, true);
            _dragOffset = new Point(0, mouseY - thumb.y);
        }
        
        protected function doDrag(event:MouseEvent):void {
            var percent:Number = (mouseY - _dragOffset.y - track.y) / availableHeight;
            position = _minPosition + percent * (_maxPosition - _minPosition);
        }
        
        protected function endDrag(event:MouseEvent):void {
            stage.removeEventListener(MouseEvent.MOUSE_MOVE, doDrag);
            stage.removeEventListener(MouseEvent.MOUSE_UP, endDrag);
            _isDragging = false;
            
            //LM: Needs review.
            // If the thumb became draggable on a track press,
            // manually generate the thumb events.
            /*if (trackDragMouseIndex > -1) {
                if (!thumb.hitTest(stage.mouseX, stage.mouseY)) {
                    //thumb.onReleaseOutside(trackDragMouseIndex);
                } else {
                    thumb.onRelease(trackDragMouseIndex);
                }
            }
            delete trackDragMouseIndex;*/
        }
        
        protected function handleTrackPress(event:MouseEvent):void {
            if (event.shiftKey || trackMode == ScrollBarTrackMode.SCROLL_TO_CURSOR) {
                var percent:Number = (mouseY - thumb.height/2 - track.y) / availableHeight;
                position = Math.round(percent * (_maxPosition - _minPosition) + _minPosition);
                
                thumb.dispatchEvent(new MouseEvent(MouseEvent.MOUSE_OVER));
                thumb.dispatchEvent(new MouseEvent(MouseEvent.MOUSE_DOWN));
                handleThumbPress(event);
                _dragOffset = new Point(0, thumb.height/2);
            }
            
            if (_isDragging || position == _trackScrollPosition) { return; }
            if (mouseY > thumb.y && mouseY < thumb.y + thumb.height) { return; }
            position += (thumb.y < mouseY) ? trackScrollPageSize : -trackScrollPageSize;
        }
        
        protected function handleTrackClick(event:ButtonEvent):void {
            if (event.isRepeat) { 
                if (_isDragging || position == _trackScrollPosition) { return; }
                if (mouseY > thumb.y && mouseY < thumb.y + thumb.height) { return; }
                position += (thumb.y < mouseY) ? trackScrollPageSize : -trackScrollPageSize;
            }
        }
        
        protected function updateScrollTarget():void {
            if (_scrollTarget == null || !enabled) { return; }
            _scrollTarget.scrollV = _position;
        }
        
        protected function handleMouseWheel(event:MouseEvent):void {
            position -= (event.delta > 0 ? 1 : -1) * _pageScrollSize;
        }
        
        override protected function changeFocus():void {
            thumb.displayFocus = _focused || _displayFocus;
        }
    }
}
